package com.mattgarner.jaddas.common;

/*
 * A ResultSet is a binary representation of a table, including
 * the header data that explains the column names and types.
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;

public class ResultSet {
	private ResultSetHeader header;
	private ArrayList<byte[]> dataRows;
	private int columnCount;
	private int[] columnDisplayWidth;
	
	public ResultSet(ResultSetHeader rsHeader) throws ResultSetException {
		if(rsHeader == null) {
			throw new ResultSetException("ResultSetHeader cannot be null.");
		}
		
		header = rsHeader;
		dataRows = new ArrayList<byte[]>();
		columnCount = header.getColumnCount();
		columnDisplayWidth = new int[columnCount];
	}
	
	public ResultSet(ResultSetHeader rsHeader, byte[] resultSetData) throws ResultSetException {
		this(rsHeader);
		importFullResultSetData(resultSetData);
	}
	
	// gets data from the binary format for a field
	public final byte[] getValue(int row, int col) throws ResultSetException {
		if(row >= dataRows.size()) {
			throw new ResultSetException("getValue: Row index ("+row+") out-of-bounds.");
		}
		if(col >= columnCount) {
			throw new ResultSetException("getValue: Column index ("+col+") out-of-bounds.");
		}
		
		byte[] rowBytes = dataRows.get(row);
		if(rowBytes == null) { rowBytes = initializeNewRow(); }
		
		// input bytes
		ByteArrayInputStream bais = new ByteArrayInputStream(rowBytes);
		DataInputStream buffer = new DataInputStream(bais);
		
		int colIndex = 0;
		
		try {
			while(colIndex < columnCount) {
				// seek data
				colIndex = buffer.readShort();
				int fieldLength = buffer.readInt();
				
				if(fieldLength != -1) {
					if(colIndex == col) {
						byte[] fieldData = new byte[fieldLength];
						buffer.read(fieldData, 0, fieldLength);
						return fieldData;
					} else {
						buffer.skip(fieldLength);
					}
				}
				colIndex++;
			}
			return null;
			
		} catch (Exception e) {
			throw new ResultSetException("getValue[byte]: Row data binary error: " +e.getMessage());
		}
	}
	
	// various repackaging of getValue method
	
	public final String getStringValue(int row, int col) throws ResultSetException {
		if(header.getColumnType(col) == ColumnType.COLUMN_TYPE_INT) {
			int value = getIntValue(row, col);
			return "" + value;
		} else if(header.getColumnType(col) == ColumnType.COLUMN_TYPE_SHORT) {
			short value = getShortValue(row, col);
			return "" + value;
		} else if(header.getColumnType(col) != ColumnType.COLUMN_TYPE_VARCHAR) { 
			throw new ResultSetException("getValue[String]: cannot get string from non-string column.");
		}
		
		try {
			byte[] fieldData = getValue(row, col);
			return new String(fieldData, header.getStringEncoding());
		} catch (UnsupportedEncodingException e ) {
			throw new ResultSetException("getValue: unsupported encoding '"+header.getStringEncoding()+"': " + e.getMessage());
		}
	}
	
	public final int getIntValue(int row, int col) throws ResultSetException {
		byte[] fieldData = getValue(row, col);
		ByteArrayInputStream bais = new ByteArrayInputStream(fieldData);
		DataInputStream buffer = new DataInputStream(bais);
		
		try {
			return buffer.readInt();
		} catch (IOException e ) {
			throw new ResultSetException("getValue[int]: exception: " + e.getMessage());
		}
	}
	
	public final short getShortValue(int row, int col) throws ResultSetException {
		byte[] fieldData = getValue(row, col);
		ByteArrayInputStream bais = new ByteArrayInputStream(fieldData);
		DataInputStream buffer = new DataInputStream(bais);
		
		try {
			return buffer.readShort();
		} catch (IOException e ) {
			throw new ResultSetException("getValue[short]: exception: " + e.getMessage());
		}
	}

	// replaces the data in a specified field
	public final void setValue(int row, int col, byte[] data) throws ResultSetException {
		if(col >= columnCount) {
			throw new ResultSetException("setValue: Column index ("+col+") out-of-bounds.");
		}
		
		byte[] rowBytes; 
		try { 
			rowBytes = dataRows.get(row);
		} catch (RuntimeException e) {
			rowBytes = initializeNewRow();
			dataRows.add(row, rowBytes);
		}
		
		// input bytes
		ByteArrayInputStream bais = new ByteArrayInputStream(rowBytes);
		DataInputStream buffer = new DataInputStream(bais);
		
		// output bytes
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream rowOutputBuffer = new DataOutputStream(baos);
		
		int colIndex = 0;
		
		try {
			
			while(colIndex < columnCount) {
				// copy the column index marker
				colIndex = buffer.readShort();
				rowOutputBuffer.writeShort(colIndex);
				

				// copy the original (or substitute new) data into new stream
				int fieldLength = buffer.readInt();
				if(colIndex == col) {
					rowOutputBuffer.writeInt(data.length);
				} else { 
					rowOutputBuffer.writeInt(fieldLength);
				}
				
				
				if(colIndex == col) {
					if(fieldLength > 0) { 
						buffer.skip(fieldLength);
					}
					rowOutputBuffer.write(data);
				} else {
					if(fieldLength > 0) {
						byte[] fieldData = new byte[fieldLength];
						buffer.read(fieldData, 0, fieldLength);
						rowOutputBuffer.write(fieldData);
					}
				}
				
				colIndex++;
			}
			rowOutputBuffer.flush();
			dataRows.set(row, baos.toByteArray());
		} catch (Exception e) {
			throw new ResultSetException("setValue[data]: Row data binary error: " +e.getMessage());
		}
	}
	
	// various methods repackaging the setValue method
	public final void setValue(int row, int col, int value) throws ResultSetException {
		byte colType = header.getColumnType(col);
		if(colType != ColumnType.COLUMN_TYPE_INT) {
			throw new ResultSetException("setValue: Tried to set int on non-integer column ("+col+").");
		}
		
		ByteArrayOutputStream baosBuffer = new ByteArrayOutputStream();
		DataOutputStream buffer = new DataOutputStream(baosBuffer);
		try {
			buffer.writeInt(value);
			buffer.close();
			baosBuffer.close();
		} catch(IOException e) {
			throw new ResultSetException("setValue[int]: Row data binary error: " +e.getMessage());
		}
		
		setValue(row, col, baosBuffer.toByteArray());
	}
	
	public final void setValue(int row, int col, short value) throws ResultSetException {
		byte colType = header.getColumnType(col);
		if(colType != ColumnType.COLUMN_TYPE_SHORT) {
			throw new ResultSetException("setValue: Tried to set short on non-short column ("+col+").");
		}
		
		ByteArrayOutputStream baosBuffer = new ByteArrayOutputStream();
		DataOutputStream buffer = new DataOutputStream(baosBuffer);
		try {
			buffer.writeShort(value);
			buffer.close();
			baosBuffer.close();
		} catch(IOException e) {
			throw new ResultSetException("setValue[int]: Row data binary error: " +e.getMessage());
		}
		
		setValue(row, col, baosBuffer.toByteArray());
	}
	
	public final void setValue(int row, int col, String value) throws ResultSetException {
		if(value == null) {
			value = "" + '\000';
		}
		byte colType = header.getColumnType(col);
		if(colType != ColumnType.COLUMN_TYPE_VARCHAR) {
			throw new ResultSetException("setValue: Tried to set string on non-string column.");
		}
		
		ByteArrayOutputStream baosBuffer = new ByteArrayOutputStream();
		DataOutputStream buffer = new DataOutputStream(baosBuffer);
		try {
			byte[] encodedString = value.getBytes(header.getStringEncoding());
			//buffer.writeInt(encodedString.length);
			buffer.write(encodedString);
			buffer.flush();
		} catch(IOException e) {
			throw new ResultSetException("setValue[String]: Row data binary error: " +e.getMessage());
		}
		
		setValue(row, col, baosBuffer.toByteArray());
	}
	
	// initialize an empty row
	public final byte[] initializeNewRow() {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream buffer = new DataOutputStream(baos);
		try { 
			for(int a=0; a<columnCount; a++) { 
					buffer.writeShort(a);
					buffer.writeInt(-1);
			}
			baos.flush();
			return baos.toByteArray();
		} catch(IOException e) {
			return null;
		}
	}
	
	// binary input methods
	private final void importFullResultSetData(byte[] inputData) {
		ByteArrayInputStream bais = new ByteArrayInputStream(inputData);
		DataInputStream buffer = new DataInputStream(bais);
		
		try {
			int rowCount = buffer.readInt();
			for(int a=0;a<rowCount;a++) {
				int rowIndex = buffer.readInt();
				int rowLength = buffer.readInt();
				byte[] rowData = new byte[rowLength];
				buffer.read(rowData, 0, rowLength);
				dataRows.add(rowIndex, rowData);
			}
		} catch(Exception e) {
			return; 
		}
	}
	
	// binary output methods
	
	public final byte[] getFullResultSetData() {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream buffer = new DataOutputStream(baos);
		
		try {
			buffer.writeInt(dataRows.size());
			
			for(int a=0; a<dataRows.size(); a++) {
				byte[] thisRowData = dataRows.get(a); 
				buffer.writeInt(a);
				buffer.writeInt(thisRowData.length);
				buffer.write(thisRowData); 
			}
			
			buffer.flush();
			return baos.toByteArray();
		} catch(IOException e) {
			// shouldn't ever happen
			return null;
		}
	}
	
	public final byte[] getSingleRowData(int index) {
		return dataRows.get(index);
	}

	
	// misc. methods
	public final ResultSetHeader getHeader() {
		return header;
	}
	
	public final int getRowCount() {
		return dataRows.size();
	}
	
	// string graphic representation methods
	
	public final String toFormattedString() {
		try { 
			String out = "";
			String tableHeader = "|";
			
			// determine max col width
			for(int b=0; b<dataRows.size(); b++) {
				for(int c=0; c<columnCount; c++) {
					String value = "";
					value = getStringValue(b, c).replace(""+'\000',"NULL");	
					columnDisplayWidth[c] = value.length() + 4;
				}
			}
			
			// columns
			for(int a=0;a<columnCount;a++) {
				String colName = header.getColumnName(a);
				int colNameLen = colName.length() + 4;
				
				if(columnDisplayWidth[a] < colNameLen) {
					columnDisplayWidth[a] = colNameLen;
				}
				tableHeader += "  " + colName + repeat(' ', columnDisplayWidth[a] - colName.length() - 2) + "  |";
			}
			out += "+" + repeat('-', tableHeader.length()-2) + "+\n";
			out += tableHeader + "\n";
			out += "+" + repeat('-', tableHeader.length()-2) + "+\n";
			
			
			for(int b=0; b<dataRows.size(); b++) {
				String rowText = "|";
				for(int c=0; c<columnCount; c++) {
					String value = "";
					value = getStringValue(b, c).replace(""+'\000',"NULL");	
					rowText += "  " + value + repeat(' ', columnDisplayWidth[c] - value.length() - 2) + "  |";
				}
				out += rowText + "\n";
			}
			out += "+" + repeat('-', tableHeader.length()-2) + "+\n";
			
			return out;
		} catch(ResultSetException e) {
			return "ResultSetException: " + e.getMessage();
		}
	}
	
	private final String repeat(char in, int count) {
		String out = "";
		for(int a=0; a<count; a++) {
			out = out + in;
		}
		return out;
	}
}
